{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "35e1b2f7",
   "metadata": {
    "toc": true
   },
   "source": [
    "<h1>Table of Contents<span class=\"tocSkip\"></span></h1>\n",
    "<div class=\"toc\"><ul class=\"toc-item\"></ul></div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81a6f12e",
   "metadata": {},
   "source": [
    "下面几段代码展示朴素贝叶斯模型的训练和预测。这里使用的数据集为本书自制的Books数据集，包含约1万本图书的标题，分为3种主题。首先是预处理，针对文本分类的预处理主要包含以下步骤：\n",
    "\n",
    "- 通常可以将英文文本全部转换为小写，或者将中文内容全部转换为简体，等等，这一般不会改变文本内容。\n",
    "- 去除标点。英文中的标点符号和单词之间没有空格（如——“Hi, there!”），如果不去除标点，“Hi,”和“there!”会被识别为不同于“Hi”和“there”的两个词，这显然是不合理的。对于中文，移除标点一般也不会影响文本的内容。\n",
    "- 分词。中文汉字之间没有空格分隔，中文分词有时比英文分词更加困难，此处不再赘述。\n",
    "- 去除停用词（如“I”、“is”、“的”等）。这些词往往大量出现但没有具体含义。\n",
    "- 建立词表。通常会忽略语料库中频率非常低的词。\n",
    "- 将词转换为词表索引（ID），便于机器学习模型使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "27cad9a7-ab9d-4503-9603-f18f3c7cb0d8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:52:27.616837Z",
     "start_time": "2025-04-16T05:52:12.020412Z"
    }
   },
   "outputs": [],
   "source": [
    "import json\n",
    "import os\n",
    "import requests\n",
    "import re\n",
    "from tqdm import tqdm\n",
    "from collections import defaultdict\n",
    "from string import punctuation\n",
    "import spacy\n",
    "from spacy.lang.zh.stop_words import STOP_WORDS\n",
    "nlp = spacy.load('zh_core_web_sm')\n",
    "# python -m spacy download zh_core_web_sm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "5936ceb0",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:54:27.605998Z",
     "start_time": "2025-04-16T05:52:27.620843Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "train size = 8627 , test size = 2157\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████████████████████████████████████████████████████████████████████████| 8627/8627 [01:27<00:00, 98.08it/s]\n",
      "100%|██████████████████████████████████████████████████████████████████████████████| 2157/2157 [00:31<00:00, 67.93it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['python', '编程', '入门', '教程']\n",
      "{'计算机类': 0, '艺术传媒类': 1, '经管类': 2}\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "class BooksDataset:\n",
    "    def __init__(self):\n",
    "        train_file, test_file = 'train.jsonl', 'test.jsonl'\n",
    "\n",
    "        # 下载数据为JSON格式，转化为Python对象\n",
    "        def read_file(file_name):\n",
    "            with open(file_name, 'r', encoding='utf-8') as fin:\n",
    "                json_list = list(fin)\n",
    "            data_split = []\n",
    "            for json_str in json_list:\n",
    "                data_split.append(json.loads(json_str))\n",
    "            return data_split\n",
    "\n",
    "        self.train_data, self.test_data = read_file(train_file),\\\n",
    "            read_file(test_file)\n",
    "        print('train size =', len(self.train_data), \n",
    "              ', test size =', len(self.test_data))\n",
    "        \n",
    "        # 建立文本标签和数字标签的映射\n",
    "        self.label2id, self.id2label = {}, {}\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                txt = data['class']\n",
    "                if txt not in self.label2id:\n",
    "                    idx = len(self.label2id)\n",
    "                    self.label2id[txt] = idx\n",
    "                    self.id2label[idx] = txt\n",
    "                label_id = self.label2id[txt]\n",
    "                data['label'] = label_id\n",
    "\n",
    "    def tokenize(self, attr='book'):\n",
    "        # 使用以下两行命令安装spacy用于中文分词\n",
    "        # pip install -U spacy\n",
    "        # python -m spacy download zh_core_web_sm\n",
    "        # 去除文本中的符号和停用词\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in tqdm(data_split):\n",
    "                # 转为小写\n",
    "                text = data[attr].lower()\n",
    "                # 符号替换为空\n",
    "                tokens = [t.text for t in nlp(text) \\\n",
    "                    if t.text not in STOP_WORDS]\n",
    "                # 这一步比较耗时，因此把tokenize的结果储存起来\n",
    "                data['tokens'] = tokens\n",
    "\n",
    "    # 根据分词结果建立词表，忽略部分低频词，\n",
    "    # 可以设置词最短长度和词表最大大小\n",
    "    def build_vocab(self, min_freq=3, min_len=2, max_size=None):\n",
    "        frequency = defaultdict(int)\n",
    "        for data in self.train_data:\n",
    "            tokens = data['tokens']\n",
    "            for token in tokens:\n",
    "                frequency[token] += 1 \n",
    "\n",
    "        print(f'unique tokens = {len(frequency)}, '+\\\n",
    "              f'total counts = {sum(frequency.values())}, '+\\\n",
    "              f'max freq = {max(frequency.values())}, '+\\\n",
    "              f'min freq = {min(frequency.values())}')    \n",
    "\n",
    "        self.token2id = {}\n",
    "        self.id2token = {}\n",
    "        total_count = 0\n",
    "        for token, freq in sorted(frequency.items(),\\\n",
    "            key=lambda x: -x[1]):\n",
    "            if max_size and len(self.token2id) >= max_size:\n",
    "                break\n",
    "            if freq > min_freq:\n",
    "                if (min_len is None) or (min_len and \\\n",
    "                    len(token) >= min_len):\n",
    "                    self.token2id[token] = len(self.token2id)\n",
    "                    self.id2token[len(self.id2token)] = token\n",
    "                    total_count += freq\n",
    "            else:\n",
    "                break\n",
    "        print(f'min_freq = {min_freq}, min_len = {min_len}, '+\\\n",
    "              f'max_size = {max_size}, '\n",
    "              f'remaining tokens = {len(self.token2id)}, '\n",
    "              f'in-vocab rate = {total_count / sum(frequency.values())}')\n",
    "\n",
    "    # 将分词后的结果转化为数字索引\n",
    "    def convert_tokens_to_ids(self):\n",
    "        for data_split in [self.train_data, self.test_data]:\n",
    "            for data in data_split:\n",
    "                data['token_ids'] = []\n",
    "                for token in data['tokens']:\n",
    "                    if token in self.token2id:\n",
    "                        data['token_ids'].append(self.token2id[token])\n",
    "\n",
    "        \n",
    "dataset = BooksDataset()\n",
    "dataset.tokenize()\n",
    "print(dataset.train_data[0]['tokens'])\n",
    "print(dataset.label2id)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26d79e05",
   "metadata": {},
   "source": [
    "完成分词后，对出现次数超过3次的词元建立词表，并将分词后的文档转化为词元id的序列。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "b3b4c04f-99e6-4b04-91c9-7603ea5f7100",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:54:27.614002Z",
     "start_time": "2025-04-16T05:54:27.609003Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['D:\\\\program\\\\anaconda', 'D:\\\\program\\\\anaconda\\\\Lib\\\\site-packages']\n"
     ]
    }
   ],
   "source": [
    "import site\n",
    "print(site.getsitepackages())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "0d6b1918",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:54:27.675337Z",
     "start_time": "2025-04-16T05:54:27.618514Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "unique tokens = 6956, total counts = 54884, max freq = 1635, min freq = 1\n",
      "min_freq = 3, min_len = 2, max_size = None, remaining tokens = 1650, in-vocab rate = 0.7944209605713869\n",
      "[18, 26, 5, 0]\n"
     ]
    }
   ],
   "source": [
    "dataset.build_vocab(min_freq=3)\n",
    "dataset.convert_tokens_to_ids()\n",
    "print(dataset.train_data[0]['token_ids'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d096d95f",
   "metadata": {},
   "source": [
    "接下来将数据和标签准备成便于训练的矩阵格式。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "ba632265",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:54:27.853341Z",
     "start_time": "2025-04-16T05:54:27.678342Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "train_X, train_Y = [], []\n",
    "test_X, test_Y = [], []\n",
    "\n",
    "for data in dataset.train_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    train_X.append(x)\n",
    "    train_Y.append(data['label'])\n",
    "for data in dataset.test_data:\n",
    "    x = np.zeros(len(dataset.token2id), dtype=np.int32)\n",
    "    for token_id in data['token_ids']:\n",
    "        x[token_id] += 1\n",
    "    test_X.append(x)\n",
    "    test_Y.append(data['label'])\n",
    "train_X, train_Y = np.array(train_X), np.array(train_Y)\n",
    "test_X, test_Y = np.array(test_X), np.array(test_Y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3938acdb",
   "metadata": {},
   "source": [
    "下面代码展示朴素贝叶斯的训练和预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "f13251b7",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:54:27.947701Z",
     "start_time": "2025-04-16T05:54:27.855701Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "P(计算机类) = 0.4453460067230787\n",
      "P(艺术传媒类) = 0.26660484525327466\n",
      "P(经管类) = 0.2880491480236467\n",
      "P(教程|计算机类) = 0.5726495726495726\n",
      "P(基础|计算机类) = 0.6503006012024048\n",
      "P(设计|计算机类) = 0.606694560669456\n",
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "\n",
    "class NaiveBayes:\n",
    "    def __init__(self, num_classes, vocab_size):\n",
    "        self.num_classes = num_classes\n",
    "        self.vocab_size = vocab_size\n",
    "        self.prior = np.zeros(num_classes, dtype=np.float64)\n",
    "        self.likelihood = np.zeros((num_classes, vocab_size),\\\n",
    "            dtype=np.float64)\n",
    "        \n",
    "    def fit(self, X, Y):\n",
    "        # NaiveBayes的训练主要涉及先验概率和似然的估计，\n",
    "        # 这两者都可以通过计数简单获得\n",
    "        for x, y in zip(X, Y):\n",
    "            self.prior[y] += 1\n",
    "            for token_id in x:\n",
    "                self.likelihood[y, token_id] += 1\n",
    "                \n",
    "        self.prior /= self.prior.sum()\n",
    "        # laplace平滑\n",
    "        self.likelihood += 1\n",
    "        self.likelihood /= self.likelihood.sum(axis=0)\n",
    "        # 为了避免精度溢出，使用对数概率\n",
    "        self.prior = np.log(self.prior)\n",
    "        self.likelihood = np.log(self.likelihood)\n",
    "    \n",
    "    def predict(self, X):\n",
    "        # 算出各个类别的先验概率与似然的乘积，找出最大的作为分类结果\n",
    "        preds = []\n",
    "        for x in X:\n",
    "            p = np.zeros(self.num_classes, dtype=np.float64)\n",
    "            for i in range(self.num_classes):\n",
    "                p[i] += self.prior[i]\n",
    "                for token in x:\n",
    "                    p[i] += self.likelihood[i, token]\n",
    "            preds.append(np.argmax(p))\n",
    "        return preds\n",
    "\n",
    "nb = NaiveBayes(len(dataset.label2id), len(dataset.token2id))\n",
    "train_X, train_Y = [], []\n",
    "for data in dataset.train_data:\n",
    "    train_X.append(data['token_ids'])\n",
    "    train_Y.append(data['label'])\n",
    "nb.fit(train_X, train_Y)\n",
    "\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2label[i]}) = {np.exp(nb.prior[i])}')\n",
    "for i in range(3):\n",
    "    print(f'P({dataset.id2token[i]}|{dataset.id2label[0]}) = '+\\\n",
    "          f'{np.exp(nb.likelihood[0, i])}')\n",
    "\n",
    "test_X, test_Y = [], []\n",
    "for data in dataset.test_data:\n",
    "    test_X.append(data['token_ids'])\n",
    "    test_Y.append(data['label'])\n",
    "    \n",
    "NB_preds = nb.predict(test_X)\n",
    "    \n",
    "for i, (p, y) in enumerate(zip(NB_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1cf6399",
   "metadata": {},
   "source": [
    "下面使用第3章介绍的TF-IDF方法得到文档的特征向量，并使用PyTorch实现逻辑斯谛回归模型的训练和预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "8679bd59-83bf-4281-8847-9fa3335eb4d8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:54:27.955309Z",
     "start_time": "2025-04-16T05:54:27.950211Z"
    }
   },
   "outputs": [],
   "source": [
    "# import utils\n",
    "# print(utils.__file__)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "21a3bc79",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:55:22.114291Z",
     "start_time": "2025-04-16T05:55:21.247031Z"
    },
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    "sys.path.append('../code')\n",
    "from my_utils1 import TFIDF  # utils 和系统中的 utils 库重名了，如上代码显示，因此可以将自己编写的 utils 重命名为 my_utils\n",
    "        \n",
    "tfidf = TFIDF(len(dataset.token2id))\n",
    "tfidf.fit(train_X)\n",
    "train_F = tfidf.transform(train_X)\n",
    "test_F = tfidf.transform(test_X)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc8af30b",
   "metadata": {},
   "source": [
    "逻辑斯谛回归可以看作一个一层的神经网络模型，使用PyTorch实现可以方便地利用自动求导功能。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "1ddebf0c",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:55:47.320307Z",
     "start_time": "2025-04-16T05:55:22.118295Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "epoch-49, loss=0.2419: 100%|█| 50/50 [00:21<00:00,  2.37it/s\n"
     ]
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAGwCAYAAABB4NqyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABbq0lEQVR4nO3dd1hT9/4H8HfClhFEBEQBEdw4KjjAvfe1tb3aatW22tba1lqt3tpl163eDmuXdmlte3uttVrrr060bpyIGxUHgsoQlL2EnN8fSEjIDklOQt6v5+F5cs75nnM+OUby4TslgiAIICIiInIgUrEDICIiIrI2JkBERETkcJgAERERkcNhAkREREQOhwkQERERORwmQERERORwmAARERGRw3EWOwBrk8vluHXrFry9vSGRSMQOh4iIiAwgCAIKCwsRHBwMqbT+9TcOlwDdunULISEhYodBREREJkhPT0eLFi3qfR2HS4C8vb0BVD9AHx8fkaMhIiIiQxQUFCAkJETxPV5fDpcA1TR7+fj4MAEiIiKyM+bqvsJO0ERERORwmAARERGRw2ECRERERA6HCRARERE5HCZARERE5HCYABEREZHDYQJEREREDocJEBERETkcJkBERETkcJgAERERkcNhAkREREQOhwkQERERORwmQGYiCALuFFfgcnah2KEQERGRHg63GrylXM0pxuBP9gIALrw3Au4uTiJHRERERNqwBshMgmUeitef7rwkYiRERESkDxMgM/Fwra3xOXezQMRIiIiISB8mQGb06zO9AAAHLueIHAkRERHpwgTIjEL9Gile776QLWIkREREpAsTIDNq3MhV8frJ1cdEjISIiIh0YQJkRsr9gACg7F6VSJEQERGRLkyAzOy3Z2MVr6/lFIsYCREREWnDBMjMeoT7KV6/9scZESMhIiIibZgAWVBSWh4EQRA7DCIiIqqDCZCFRb+/U+wQiIiIqA4mQBbw14t9FK/vFFeIGAkRERFpwgTIAqKay8QOgYiIiHRgAmQh84e3VbyukrMfEBERkS1hAmQhPZVGgz3xw1ERIyEiIqK6mABZSJcQX8Xr/Sk5HA1GRERkQ5gAWYiLk+qjvZxdJFIkREREVBcTIAtKfneE4vXQT/eJGAkREREpYwJkQXXXBiMiIiLbwATIihKu5IgdAhEREYEJkFVN+u6I2CEQERERmAAR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WHz77bdIS0vDzJkzAVQ3X928eRM//fQTgOrkZ+rUqfjss8/Qq1cvRe2Rh4cHZDLTs2IiIrJ9LRo30l/IQpp4ueHbKdFwd3Gq18SQmvoSSaWSeic/+prFOrfwVdneObc/fj6UiucHRiLAxx1yuaDxfUmlEvh5uuJOcQUimnqqHVfmLJWg0sSJJsUgagI0ceJE5Obm4t1330VGRgaioqKwZcsWhIWFAQAyMjKQllZb9fbNN9+gsrISzz//PJ5//nnF/mnTpmH16tXWDp+IiBzIsI71H8nl7V77taurs7Ghjr8xBPml99BM5qGzXN2Ww8gAL7wzLkqxrSup279gIDaduoXB7bX3twWAIe0DsezRrnhj41lM7G77fW0lgqELnjQQBQUFkMlkyM/Ph4+PYR3JiIiIzKWkohKCAHi6Wb4OouWrmwEA0WGNsf65OLNfV9moTkFYPtlyM1mb+/u74TbuERER2aBGDbRfjdTCE1qam+hrgREREZFlWSM1cbLQormWwgSIiIiogbNG5QxrgIiIiMimSMxcB/TVpG5wc5bimynRGHG/c/hTvcPNeg9La5gNkURERKTg5mLe+o7RnZthRFQQnKQSDOsQiIIyy85jZAmsASIiImqglozvhHB/T7ynNOTdXGr6/Egk9Z/HSAysASIiImqgHu0Rikd7hOov6IBYA0REREQOhwkQERERORwmQERERORwmAARERGRw2ECRERERA6HCRARERE5HCZARERE5HCYABEREZHDYQJEREREDocJEBERETkcJkBERETkcJgAERERkcNhAkREREQOhwkQERERORxnsQOwNkEQAAAFBQUiR0JERESGqvnervkery+HS4AKCwsBACEhISJHQkRERMYqLCyETCar93UkgrlSKTshl8tx69YteHt7QyKRmPXaBQUFCAkJQXp6Onx8fMx6bXvC51CLz6Ian0M1PodafBbV+ByqGfIcBEFAYWEhgoODIZXWvwePw9UASaVStGjRwqL38PHxcegPcg0+h1p8FtX4HKrxOdTis6jG51BN33MwR81PDXaCJiIiIofDBIiIiIgcDhMgM3Jzc8OiRYvg5uYmdiii4nOoxWdRjc+hGp9DLT6LanwO1cR4Dg7XCZqIiIiINUBERETkcJgAERERkcNhAkREREQOhwkQERERORwmQGayfPlyhIeHw93dHdHR0di/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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "eval_loss = 0.2870\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "\n",
    "class LR(nn.Module):\n",
    "    def __init__(self, input_dim, output_dim):\n",
    "        super(LR, self).__init__()\n",
    "        self.linear = nn.Linear(input_dim, output_dim)\n",
    "        \n",
    "    def forward(self, input_feats, labels=None):\n",
    "        outputs = self.linear(input_feats)\n",
    "        \n",
    "        if labels is not None:\n",
    "            loss_fc = nn.CrossEntropyLoss()\n",
    "            loss = loss_fc(outputs, labels)\n",
    "            return (loss, outputs)\n",
    "        \n",
    "        return outputs\n",
    "\n",
    "model = LR(len(dataset.token2id), len(dataset.label2id))\n",
    "\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torch.optim import SGD, Adam\n",
    "\n",
    "# 使用PyTorch的DataLoader来进行数据循环，因此按照PyTorch的接口\n",
    "# 实现myDataset和DataCollator两个类\n",
    "# myDataset是对特征向量和标签的简单封装便于对齐接口，\n",
    "# DataCollator用于批量将数据转化为PyTorch支持的张量类型\n",
    "class myDataset(Dataset):\n",
    "    def __init__(self, X, Y):\n",
    "        self.X = X\n",
    "        self.Y = Y\n",
    "        \n",
    "    def __len__(self):\n",
    "        return len(self.X)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        return (self.X[idx], self.Y[idx])\n",
    "\n",
    "class DataCollator:\n",
    "    @classmethod\n",
    "    def collate_batch(cls, batch):\n",
    "        feats, labels = [], []\n",
    "        for x, y in batch:\n",
    "            feats.append(x)\n",
    "            labels.append(y)\n",
    "        # 直接将一个ndarray的列表转化为张量是非常慢的，\n",
    "        # 所以需要提前将列表转化为一整个ndarray\n",
    "        feats = torch.tensor(np.array(feats), dtype=torch.float)\n",
    "        labels = torch.tensor(np.array(labels), dtype=torch.long)\n",
    "        return {'input_feats': feats, 'labels': labels}\n",
    "\n",
    "# 设置训练超参数和优化器，模型初始化\n",
    "epochs = 50\n",
    "batch_size = 128\n",
    "learning_rate = 1e-3\n",
    "weight_decay = 0\n",
    "\n",
    "train_dataset = myDataset(train_F, train_Y)\n",
    "test_dataset = myDataset(test_F, test_Y)\n",
    "\n",
    "data_collator = DataCollator()\n",
    "train_dataloader = DataLoader(train_dataset, batch_size=batch_size,\\\n",
    "    shuffle=True, collate_fn=data_collator.collate_batch)\n",
    "test_dataloader = DataLoader(test_dataset, batch_size=batch_size,\\\n",
    "    shuffle=False, collate_fn=data_collator.collate_batch)\n",
    "optimizer = Adam(model.parameters(), lr=learning_rate,\\\n",
    "    weight_decay=weight_decay)\n",
    "model.zero_grad()\n",
    "model.train()\n",
    "\n",
    "from tqdm import tqdm, trange\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# 模型训练\n",
    "with trange(epochs, desc='epoch', ncols=60) as pbar:\n",
    "    epoch_loss = []\n",
    "    for epoch in pbar:\n",
    "        model.train()\n",
    "        for step, batch in enumerate(train_dataloader):\n",
    "            loss = model(**batch)[0]\n",
    "            pbar.set_description(f'epoch-{epoch}, loss={loss.item():.4f}')\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            model.zero_grad()\n",
    "            epoch_loss.append(loss.item())\n",
    "\n",
    "    epoch_loss = np.array(epoch_loss)\n",
    "    # 打印损失曲线\n",
    "    plt.plot(range(len(epoch_loss)), epoch_loss)\n",
    "    plt.xlabel('training epoch')\n",
    "    plt.ylabel('loss')\n",
    "    plt.show()\n",
    "    \n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        loss_terms = []\n",
    "        for batch in test_dataloader:\n",
    "            loss = model(**batch)[0]\n",
    "            loss_terms.append(loss.item())\n",
    "        print(f'eval_loss = {np.mean(loss_terms):.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10808854",
   "metadata": {},
   "source": [
    "下面的代码使用训练好的模型对测试集进行预测，并报告分类结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "11a9bf62",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:55:47.415185Z",
     "start_time": "2025-04-16T05:55:47.324819Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test example-0, prediction = 0, label = 0\n",
      "test example-1, prediction = 0, label = 0\n",
      "test example-2, prediction = 1, label = 1\n",
      "test example-3, prediction = 1, label = 1\n",
      "test example-4, prediction = 1, label = 1\n"
     ]
    }
   ],
   "source": [
    "LR_preds = []\n",
    "model.eval()\n",
    "for batch in test_dataloader:\n",
    "    with torch.no_grad():\n",
    "        _, preds = model(**batch)\n",
    "        preds = np.argmax(preds, axis=1)\n",
    "        LR_preds.extend(preds)\n",
    "            \n",
    "for i, (p, y) in enumerate(zip(LR_preds, test_Y)):\n",
    "    if i >= 5:\n",
    "        break\n",
    "    print(f'test example-{i}, prediction = {p}, label = {y}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5feb65e",
   "metadata": {},
   "source": [
    "下面的代码展示多分类情况下宏平均和微平均的算法。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "a5ac32c5",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-04-16T05:55:47.463457Z",
     "start_time": "2025-04-16T05:55:47.417200Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "NB: micro-f1 = 0.8961520630505331, macro-f1 = 0.8948572078813896\n",
      "LR: micro-f1 = 0.9142327306444136, macro-f1 = 0.9134964833213877\n"
     ]
    }
   ],
   "source": [
    "test_Y = np.array(test_Y)\n",
    "NB_preds = np.array(NB_preds)\n",
    "LR_preds = np.array(LR_preds)\n",
    "\n",
    "def micro_f1(preds, labels):\n",
    "    TP = np.sum(preds == labels)\n",
    "    FN = FP = 0\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        FN += np.sum((preds == i) & (labels != i))\n",
    "        FP += np.sum((preds != i) & (labels == i))\n",
    "    precision = TP / (TP + FP)\n",
    "    recall = TP / (TP + FN)\n",
    "    f1 = 2 * precision * recall / (precision + recall)\n",
    "    return f1\n",
    "\n",
    "def macro_f1(preds, labels):\n",
    "    f_scores = []\n",
    "    for i in range(len(dataset.label2id)):\n",
    "        TP = np.sum((preds == i) & (labels == i))\n",
    "        FN = np.sum((preds == i) & (labels != i))\n",
    "        FP = np.sum((preds != i) & (labels == i))\n",
    "        precision = TP / (TP + FP)\n",
    "        recall = TP / (TP + FN)\n",
    "        f1 = 2 * precision * recall / (precision + recall)\n",
    "        f_scores.append(f1)\n",
    "    return np.mean(f_scores)\n",
    "\n",
    "print(f'NB: micro-f1 = {micro_f1(NB_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(NB_preds, test_Y)}')\n",
    "print(f'LR: micro-f1 = {micro_f1(LR_preds, test_Y)}, '+\\\n",
    "      f'macro-f1 = {macro_f1(LR_preds, test_Y)}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "937fdc5c-3b90-455a-91f4-1202fbf514dc",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2a30fad8",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1e97881e",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dd47f522",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "95823621",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f8b173b2",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97f2c705",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": false,
   "sideBar": false,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": true,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
